/**
 * 检测规范模型
 *
 * Created by fangwenyu on 2021/03/08.
 */
const mongoose = require('mongoose');
const path = require('path');
const fs = require('fs-extra');
const parse = require('csv-parse');
const mongoClient = require('./mongo/client');
const Rule = require('./mongo/rule');
const RuleSet = require('./mongo/rule-set');
const FileDefect = require('./mongo/file-defect');
const RuleStat = require('./mongo/rule-stat');
const AnalyzeTemplate = require('./mongo/analyze-template');
const utility = require('./utility');
const common = require('./common');
const logger = require('./logger');
const License = require('../models/license');
const analyzeTemplate = require('./analyze-templates');

const ruleLevel = {
  mandatory: 1, // 强制
  required: 2, // 要求
  advisory: 3, // 建议
  recommend: 4, // 推荐
  necessary: 5, // 必要
  highlyRecommended: 6, // 强烈推荐
  designTaboo: 7, // 设计禁忌
};
const ruleLevelEum = {
  1: {
    name: '强制',
    subLevelOrder: 1,
    mainLevel: 1,
  },
  2: {
    name: '要求',
    subLevelOrder: 2,
    mainLevel: 2,
  },
  3: {
    name: '建议',
    subLevelOrder: 5,
    mainLevel: 3,
  },
  4: {
    name: '推荐',
    subLevelOrder: 6,
    mainLevel: 3,
  },
  5: {
    name: '必要',
    subLevelOrder: 3,
    mainLevel: 2,
  },
  6: {
    name: '强烈推荐',
    subLevelOrder: 4,
    mainLevel: 2,
  },
  7: {
    name: '设计禁忌',
    subLevelOrder: 1,
    mainLevel: 7,
  },
};

const ruleExtraConfig = {
  Enable_Partial_Symbolic_Execution: {
    type: 'bool',
    default: false,
    desc: '启用部分符号执行',
    rules: ['CWE-369'],
  },
  Enable_Reporting_Potential_Division_By_Zero: {
    type: 'bool',
    default: false,
    desc: '启用报告可能为0的除数',
    rules: ['CWE-369'],
  },
  Do_Not_Report_Assignment_Statements_In_Parentheses_As_Single_Conditions: {
    type: 'bool',
    default: true,
    desc: '不报告作为单一条件的在括号中的赋值语句',
    rules: ['CWE-481'],
  },
  Do_Not_Report_Parameters_Of_The_Main_Function: {
    type: 'bool',
    default: true,
    desc: '不报告main函数的参数',
    rules: ['GJB5369-2005-1.2.1'],
  },
  Prohibited_Name_Set: {
    type: 'string',
    default: 'FILEcount,cin,endl',
    desc: '禁止的名称集合',
    rules: ['GJB8114-2013-R-1-1-4'],
  },
  Maximum_Number_Of_Additional_Information_Items: {
    type: 'int',
    default: 5,
    desc: '最大额外信息条数',
    rules: ['MISRAC-2004-2.2'],
  },
  Enable_Reporting_Program_Exit_Functions: {
    type: 'bool',
    default: false,
    desc: '启用报告程序退出函数',
    rules: ['BugFinder-16.5'],
  },
  Enable_Reporting_Issues_Related_To_Memory_Allocation_Functions: {
    type: 'bool',
    default: true,
    desc: '启用报告内存分配函数相关问题',
    rules: ['MISRAC++2008-5-2-8'],
  },
  Enable_Reporting_Only_Value_Binding_Statements: {
    type: 'bool',
    default: true,
    desc: '开启仅报告值绑定语句',
    rules: ['SJT11682-2017-6.2.1'],
  },
  Allow_Only_Comments_In_Branches: {
    type: 'bool',
    default: false,
    desc: '允许分支中只有注释',
    rules: ['SJT11682-2017-6.2.1'],
  },
};

/**
 * 获取检测模板信息
 * @param {Array<ObjectId>>} ruleIds 规则Id数组
 * @param {String} ruleSetId 规则集Id
 * @return {Object}
 */
function getRulesByIds(ruleIds, ruleSetId) {
  const doc = {};
  if (ruleIds) {
    doc._id = { $in: ruleIds };
  }
  if (ruleSetId) {
    doc.ruleSetId = mongoose.Types.ObjectId(ruleSetId);
  }
  return Rule.find(doc).lean();
}
/**
 * 许可证过滤规则集
 * @returns 经过过滤的规则集id
 */
async function filterRuleSetByLicense() {
  const licenseRuleSets = await License.getStaticAnalyzeRuleSetByLicense();
  const ruleSetIds = (await RuleSet.find({
    name: { $in: licenseRuleSets },
  })).map((item) => item._id);
  return ruleSetIds;
}

async function getRules({
  languages,
  keywords,
} = {}) {
  // 对点字符进行转义
  if (keywords) {
    keywords = utility.escapeMongoExprSpecialWord(keywords);
  }
  const ruleSetIds = await filterRuleSetByLicense();
  const sets = await Rule.aggregate([
    {
      $match: {
        ruleSetId: { $in: ruleSetIds },
      },
    },
    {
      $lookup: {
        from: 'rule-sets', localField: 'ruleSetId', foreignField: '_id', as: 'ruleSet',
      },
    },
    {
      $unwind: '$ruleSet',
    },
    {
      $match: languages && languages.length ? {
        'ruleSet.languages': { $elemMatch: { $in: languages } },
      } : {},
    },
    {
      $match: keywords ? {
        $or: [
          { name: new RegExp(keywords) },
          { ruleCode: new RegExp(keywords) },
          { 'ruleSet.name': new RegExp(keywords) },
        ],
      } : {},
    },
    {
      $group: {
        _id: '$ruleSetId',
        total: { $sum: 1 },
        ruleSetId: { $first: '$ruleSetId' },
        ruleSetName: { $first: '$ruleSet.name' },
        rules: {
          $push: {
            level: '$level',
            ruleId: '$_id',
            ruleName: '$name',
            ruleCode: '$ruleCode',
            originalLevel: '$originalLevel',
            orderIndex: '$orderIndex',
            externalRuleCode: '$externalRuleCode',
            hcscliExternalRuleCode: '$hcscliExternalRuleCode',
          },
        },
      },
    },
    {
      $sort: {
        ruleSetName: 1,
      },
    },
  ]);
  return sets;
}

/**
 * 获取检测模板信息
 * @param {String} ruleId 规则Id
 * @return {Object}
 */
async function getRuleObject(ruleId) {
  const rid = mongoose.Types.ObjectId(ruleId);
  const [ret] = await Rule.aggregate([
    {
      $match: { _id: rid },
    },
    {
      $lookup: {
        from: 'rule-sets', localField: 'ruleSetId', foreignField: '_id', as: 'ruleSet',
      },
    },
    {
      $unwind: '$ruleSet',
    },
    {
      $addFields: {
        ruleId: '$_id',
        ruleSetName: '$ruleSet.name',
      },
    },
    {
      $unset: ['ruleSet', '_id', '__v'],
    },
  ]);
  // 补充具体规则的等级名称
  if (ret && ret.originalLevel && ruleLevelEum[ret.originalLevel].name) {
    ret.levelName = ruleLevelEum[ret.originalLevel].name;
  }
  return ret;
}

/**
 * 对导入数据进行预处理
 * @param {Object} rule 规范数据
 * @return {Object}
 */
function prepareInitData(rule) {
  switch (rule.level) {
    case '强制':
      rule.level = ruleLevel.mandatory;
      rule.originalLevel = ruleLevel.mandatory;
      break;
    case '要求':
      rule.level = ruleLevel.required;
      rule.originalLevel = ruleLevel.required;
      break;
    case '必要':
      rule.level = ruleLevel.required;
      rule.originalLevel = ruleLevel.necessary;
      break;
    case '强烈推荐':
      rule.level = ruleLevel.required;
      rule.originalLevel = ruleLevel.highlyRecommended;
      break;
    case '建议':
      rule.level = ruleLevel.advisory;
      rule.originalLevel = ruleLevel.advisory;
      break;
    case '推荐':
      rule.level = ruleLevel.advisory;
      rule.originalLevel = ruleLevel.recommend;
      break;
    case '设计禁忌':
      rule.level = ruleLevel.designTaboo;
      rule.originalLevel = ruleLevel.designTaboo;
      break;
    default:
      break;
  }
  delete rule.ruleSet;
  delete rule.languages;
  return rule;
}

/**
 * 更新文件的缺陷数量
 * @param {String|Array} ruleIds 规则Id
 * @return {Object}
 */
function updateFileDefectsCount(ruleIds, versionId) {
  if (versionId) {
    versionId = mongoose.Types.ObjectId(versionId);
  }
  if (!utility.isArrayType(ruleIds)) {
    ruleIds = [mongoose.Types.ObjectId(ruleIds)];
  } else {
    ruleIds = ruleIds.map((id) => mongoose.Types.ObjectId(id));
  }
  const rulesMap = new Map();
  return Rule.find({ _id: { $in: ruleIds } }, ['level'])
    .then((rules) => {
      rules.forEach((rule) => {
        rulesMap.set(rule._id.toString(), rule.level);
      });
      if (versionId) {
        return FileDefect.find({ ruleId: { $in: ruleIds }, versionId }, ['ruleId', 'fileId']);
      }
      return FileDefect.find({ ruleId: { $in: ruleIds } }, ['ruleId', 'fileId']);
    })
    .then((fileDefects) => {
      const promise = [];
      const filesIncDocMap = new Map();
      fileDefects.forEach((defect) => {
        const fileId = defect.fileId.toString();
        if (!filesIncDocMap.has(fileId)) {
          filesIncDocMap.set(fileId, {
            $inc: {
              'defectsCount.mandatory': 0,
              'defectsCount.required': 0,
              'defectsCount.advisory': 0,
              'defectsCount.designTaboo': 0,
            },
          });
        }
        const level = defect.level || rulesMap.get(defect.ruleId.toString());
        switch (level) {
          case ruleLevel.mandatory:
            filesIncDocMap.get(fileId).$inc['defectsCount.mandatory'] -= 1;
            break;
          case ruleLevel.required:
            filesIncDocMap.get(fileId).$inc['defectsCount.required'] -= 1;
            break;
          case ruleLevel.advisory:
            filesIncDocMap.get(fileId).$inc['defectsCount.advisory'] -= 1;
            break;
          case ruleLevel.designTaboo:
            filesIncDocMap.get(fileId).$inc['defectsCount.designTaboo'] -= 1;
            break;
          default:
            break;
        }
      });
      filesIncDocMap.forEach((incDoc, fileId) => {
        promise.push(common.increaseDirectories(fileId, incDoc));
      });
      return Promise.all(promise);
    });
}

const TJDW230RluesNotImplementedByHcscli = [
  '1.1', '1.2', '1.3', '2.1', '2.2', '2.4', '4.1', '7.6', '7.7', '7.10', '11.5',
];
const TJDW230RluesNotImplementedByCertify = [
  '1.2', '2.1', '2.2', '7.10', '16.2', '16.3', '17.1', '17.8',
];

async function getTJDW230RedundantRluesByLicense() {
  const sysLicense = await License.getLicense();
  if (!sysLicense || !sysLicense.modules || !sysLicense.modules.staticAnalyze || !sysLicense.modules.staticAnalyze.modules) {
    return [];
  }
  let redundantRulesCode = [];
  if (sysLicense.modules.staticAnalyze.modules.hcscli && sysLicense.modules.staticAnalyze.modules.hcscli.specs.indexOf('TJ_DW230-2020-C') > -1) {
    // hcscli
    redundantRulesCode = TJDW230RluesNotImplementedByHcscli;
  } else if (
    sysLicense.modules.staticAnalyze.modules.certify && sysLicense.modules.staticAnalyze.modules.certify.specs.indexOf('TJ_DW230-2020-C') > -1
  ) {
    // certify
    redundantRulesCode = TJDW230RluesNotImplementedByCertify;
  }
  const setId = (await RuleSet.findOne({ name: 'TJ_DW230-2020-C' }, { _id: 1 }))._id;
  if (!setId) {
    return [];
  }
  const rules = (await Rule.find({
    ruleSetId: setId,
    ruleCode: { $in: redundantRulesCode },
  }, { _id: 1 })).map((rule) => rule._id);
  return rules;
}

async function removeRulesAndRuleSets(ruleIds, ruleSetIds) {
  await updateFileDefectsCount(ruleIds);
  return Promise.all([
    Rule.deleteMany({ _id: { $in: ruleIds } }),
    FileDefect.deleteMany({ ruleId: { $in: ruleIds } }),
    RuleStat.deleteMany({ ruleId: { $in: ruleIds } }),
    AnalyzeTemplate.updateMany({}, { $pullAll: { rules: ruleIds } }),
    RuleSet.deleteMany({ _id: { $in: ruleSetIds } }),
  ]);
}
/**
 * 初始化静态分析文件规则集数据
 * @param {String} rulesPath 数据路径
 * @return {Object}
 */
function initRulesByFile(fileName) {
  const ruleSetName = fileName.substring(0, fileName.lastIndexOf('.'));
  logger.debug(`read rule file:${fileName}`);
  const filePath = path.join(__dirname, '..', '..', global.config.path.analyzeRulesDir, fileName);
  if (!fs.existsSync(filePath)) {
    logger.debug(`File not found, path: ${filePath}`);
    return Promise.resolve();
  }
  let ruleSet = null;
  const rulesMap = new Map();
  const updateRules = {
    insert: [],
    update: [],
  };
  return RuleSet.findOne({ name: ruleSetName })
    .then((ruleSetDb) => {
      if (ruleSetDb) {
        ruleSet = ruleSetDb.toObject();
        return Rule.find({ ruleSetId: ruleSet._id }, ['_id', 'ruleSetId', 'ruleCode']);
      }
      return [];
    })
    .then((rules) => {
      rules.forEach((rule) => {
        rulesMap.set(`${rule.ruleSetId.toString()}:${rule.ruleCode}`, rule._id);
      });
      logger.debug(`Loading csv file:${fileName}`);
    })
    // load csv
    .then(() => new Promise((resolve, reject) => {
      let orderIndex = 1;
      return fs.createReadStream(filePath)
        .pipe(parse({ delimiter: ';', columns: true, trim: true }))
        .on('data', (row) => {
          const { ruleSet: ruleSetName, ruleCode } = row;
          row.orderIndex = orderIndex;
          orderIndex += 1;
          const languages = ruleSetName === 'CWE' ? ['c', 'c++', 'java'] : row.languages.split(',');
          if (!ruleSet || !ruleSet._id) {
            ruleSet = { name: ruleSetName, languages };
            updateRules.insert.push(row);
            return;
          }
          row.ruleSetId = ruleSet._id;
          ruleSet.languages = languages;
          if (row.inTemplate) {
            row.inTemplate = row.inTemplate === '1';
          }
          const uniqueCode = `${ruleSet._id.toString()}:${ruleCode}`;
          if (!rulesMap.has(uniqueCode)) {
            updateRules.insert.push(row);
            return;
          }
          row.id = rulesMap.get(uniqueCode);
          updateRules.update.push(row);
          // remove the exist data in map,so finally, data which left in map should be delete.
          rulesMap.delete(uniqueCode);
        })
        .on('end', () => {
          logger.debug(`Load csv completed:${fileName}`);
          resolve();
        })
        .on('error', (err) => {
          logger.debug(`Load csv completed error :${fileName}`);
          reject(err);
        });
    }))
    // update exists rule sets
    .then(() => {
      if (!ruleSet._id) {
        return Promise.resolve();
      }
      return RuleSet.updateOne({ _id: ruleSet._id }, {
        languages: ruleSet.languages,
      });
    })
    // create new rule sets
    .then(() => {
      if (ruleSet._id) {
        return Promise.resolve([ruleSet]);
      }
      logger.debug(`Create ${fileName} rule sets.`);
      return RuleSet.insertMany([ruleSet]);
    })
    // upsert rules
    .then(([ruleSet]) => {
      logger.debug(`Upsert ${fileName} rules.`);
      const promise = [];
      logger.debug(`${fileName} rule insert count:${updateRules.insert.length} `);
      if (updateRules.insert.length > 0) {
        updateRules.insert.map((rule) => {
          rule.ruleSetId = ruleSet._id;
          return prepareInitData(rule);
        });
        promise.push(Rule.insertMany(updateRules.insert));
      }
      logger.debug(`${fileName} rule update count:${updateRules.update.length}`);
      if (updateRules.update.length > 0) {
        updateRules.update.map(
          (rule) => promise.push(Rule.updateOne({ _id: rule.id }, prepareInitData(rule)))
        );
      }
      return Promise.all(promise);
    })
    // delete rules
    .then(async () => {
      const deleteRuleIds = [];
      rulesMap.forEach((id) => {
        deleteRuleIds.push(id);
      });
      if (fileName === 'TJ_DW230-2020-C.csv') {
        const redundantRules = await getTJDW230RedundantRluesByLicense();
        redundantRules.forEach((id) => {
          deleteRuleIds.push(id);
        });
      }
      if (deleteRuleIds.length === 0) {
        return Promise.resolve();
      }
      logger.debug('Remove useless rules.');
      return removeRulesAndRuleSets(deleteRuleIds, []);
    });
}

/**
 * 初始化多规则集的文件
 */
function initRules() {
  if (!global.config.path.analyzeRulesDir) {
    logger.debug('Can not find directory where data of analyze rules stored.');
    return Promise.resolve();
  }
  let ruleFiles;
  const deleteRuleSetIds = [];
  const deleteRuleIds = [];
  return fs.readdir(path.join(__dirname, '..', '..', global.config.path.analyzeRulesDir))
    .then((files) => {
      ruleFiles = files.filter((fileName) => fileName.substring(
        fileName.lastIndexOf('.') + 1, fileName.length
      ) === 'csv');
    })
    .then(() => mongoClient.connect())
    .then(() => Promise.all(ruleFiles.map(
      (fileName) => initRulesByFile(fileName)
    )))
    .then(() => RuleSet.find({}))
    .then((allRuleSet) => {
      // 清除被移除的规则文件对应的规则集和规则
      allRuleSet.forEach((item) => {
        if (!ruleFiles.includes(`${item.name}.csv`)) {
          deleteRuleSetIds.push(item.id);
        }
      });
      return Rule.find({ ruleSetId: { $in: deleteRuleSetIds } });
    })
    .then((rules) => {
      if (rules.length === 0) {
        return Promise.resolve();
      }
      rules.forEach((rule) => {
        deleteRuleIds.push(rule._id);
      });
      logger.debug('Remove useless rules and rule sets.');
      return removeRulesAndRuleSets(deleteRuleIds, deleteRuleSetIds);
    })
    .then(() => mongoClient.disconnect());
}
async function getRuleSetsByTemplateId(templateId) {
  let ruleSets = [];
  const template = await AnalyzeTemplate.findById(templateId);
  // 默认的模板
  if (template && template.isDefault && template.name.startsWith(analyzeTemplate.defaultTemplatePrefix)) {
    ruleSets = await RuleSet.aggregate([{ $project: { ruleSetName: '$name' } }]);
  }
  if (template && template.rules && template.rules.length > 0) {
    ruleSets = await Rule.aggregate([
      {
        $match: {
          _id: { $in: template.rules },
        },
      },
      {
        $group: { _id: '$ruleSetId' },
      },
      {
        $lookup: {
          from: 'rule-sets',
          localField: '_id',
          foreignField: '_id',
          as: 'ruleSets',
        },
      },
      {
        $unwind: '$ruleSets',
      }, {
        $project: {
          ruleSetName: '$ruleSets.name',
        },
      },
    ]);
  }
  return ruleSets;
}

function formatRulesForUserDefined(rule, userDefined) {
  if (userDefined && userDefined[rule.ruleId]) {
    rule.originalLevel = userDefined[rule.ruleId].level;
    rule.level = ruleLevelEum[rule.originalLevel].mainLevel;
    if (rule.levelName) {
      rule.levelName = ruleLevelEum[rule.originalLevel].name;
    }
  }
  rule.subLevelOrder = ruleLevelEum[rule.originalLevel].subLevelOrder;
  return rule;
}

module.exports = {
  getRuleObject,
  getRules,
  getRulesByIds,
  getRuleSetsByTemplateId,
  formatRulesForUserDefined,

  initRules,
  initRulesByFile,

  ruleLevel,
  ruleLevelEum,
  updateFileDefectsCount,
  filterRuleSetByLicense,
  getTJDW230RedundantRluesByLicense,

  ruleExtraConfig,
};
exports = Object.assign(exports, module.exports);
